Komplexní průvodce generiky v TypeScriptu, který pokrývá jejich syntaxi, výhody, pokročilé použití a osvědčené postupy pro práci s komplexními datovými typy.
TypeScript Generics: Zvládnutí komplexních datových typů pro robustní aplikace
TypeScript, nadmnožina JavaScriptu, umožňuje vývojářům psát robustnější a udržovatelnější kód díky statickému typování. Mezi jeho nejmocnější funkce patří generika, která vám umožňují psát kód, jenž může pracovat s různými datovými typy a přitom stále zachovávat typovou bezpečnost. Tento průvodce poskytuje komplexní pohled na generika v TypeScriptu se zaměřením na jejich aplikaci na komplexní datové typy v kontextu globálního vývoje softwaru.
Co jsou generika?
Generika poskytují způsob, jak psát znovupoužitelný kód, který může pracovat s různými typy. Místo psaní samostatných funkcí nebo tříd pro každý typ, který chcete podporovat, můžete napsat jedinou funkci nebo třídu, která používá typové parametry. Tyto typové parametry jsou zástupnými symboly pro skutečné typy, které budou použity při volání nebo instanciování funkce či třídy. To je obzvláště užitečné při práci s komplexními datovými strukturami, kde se typ dat v těchto strukturách může lišit.
Výhody používání generik
- Znovupoužitelnost kódu: Napište kód jednou a použijte ho s různými typy. Tím se snižuje duplicita kódu a vaše kódová základna je udržovatelnější.
- Typová bezpečnost: Generika umožňují kompilátoru TypeScriptu vynutit typovou bezpečnost při kompilaci. To pomáhá předcházet běhovým chybám souvisejícím s nesouladem typů.
- Zlepšená čitelnost: Generika činí váš kód čitelnějším tím, že jasně označují typy, se kterými jsou vaše funkce a třídy navrženy pro práci.
- Zvýšený výkon: V některých případech mohou generika vést ke zlepšení výkonu, protože kompilátor může optimalizovat generovaný kód na základě konkrétních použitých typů.
Základní syntaxe generik
Základní syntaxe generik zahrnuje použití lomených závorek (< >) k deklaraci typových parametrů. Tyto typové parametry se obvykle nazývají T
, K
, V
atd., ale můžete použít jakýkoli platný identifikátor. Zde je jednoduchý příklad generické funkce:
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);
console.log(myString); // Výstup: hello
console.log(myNumber); // Výstup: 123
console.log(myBoolean); // Výstup: true
V tomto příkladu <T>
deklaruje typový parametr s názvem T
. Funkce identity
přijímá argument typu T
a vrací hodnotu typu T
. Při volání funkce můžete explicitně specifikovat typový parametr (např. identity<string>
) nebo nechat TypeScript, aby ho odvodil na základě typu argumentu.
Práce s komplexními datovými typy
Generika se stávají obzvláště cennými při práci s komplexními datovými typy, jako jsou pole, objekty a rozhraní. Pojďme se podívat na některé běžné scénáře:
Generická pole
Generika můžete použít k vytváření funkcí nebo tříd, které pracují s poli různých typů:
function arrayToString<T>(arr: T[]): string {
return arr.join(", ");
}
let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];
console.log(arrayToString(numberArray)); // Výstup: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Výstup: apple, banana, cherry
Zde funkce arrayToString
přijímá pole typu T[]
a vrací řetězcovou reprezentaci pole. Tato funkce funguje s poli jakéhokoli typu, což ji činí vysoce znovupoužitelnou.
Generické objekty
Generika lze také použít k definování funkcí nebo tříd, které pracují s objekty různých tvarů:
interface Person {
name: string;
age: number;
country: string; // Přidána země pro globální kontext
}
interface Product {
id: number;
name: string;
price: number;
currency: string; // Přidána měna pro globální kontext
}
function displayInfo<T extends { name: string }>(item: T): void {
console.log(`Name: ${item.name}`);
}
let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };
displayInfo(person); // Výstup: Name: Alice
displayInfo(product); // Výstup: Name: Laptop
V tomto příkladu funkce displayInfo
přijímá objekt typu T
, který musí mít vlastnost name
typu string. Klauzule extends { name: string }
je omezení (constraint), které specifikuje minimální požadavky na typový parametr T
. Tím je zajištěno, že funkce může bezpečně přistupovat k vlastnosti name
.
Pokročilé použití generik
TypeScript generika nabízejí pokročilejší funkce, které vám umožňují vytvářet ještě flexibilnější a výkonnější kód. Pojďme se na některé z těchto funkcí podívat:
Více typových parametrů
Můžete definovat funkce nebo třídy s více typovými parametry:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
interface Name {
firstName: string;
}
interface Age {
age: number;
}
const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };
const merged = merge(person, details);
console.log(merged.firstName); // Výstup: Bob
console.log(merged.age); // Výstup: 42
Funkce merge
přijímá dva objekty typů T
a U
a vrací nový objekt, který obsahuje vlastnosti obou objektů. Je to mocný způsob, jak kombinovat data z různých zdrojů.
Generická omezení
Jak bylo ukázáno dříve, omezení vám umožňují omezit typy, které lze použít s generickým typovým parametrem. Tím je zajištěno, že generický kód může bezpečně pracovat se specifikovanými typy.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity([1, 2, 3]); // Výstup: 3
loggingIdentity("hello"); // Výstup: 5
// loggingIdentity(123); // Chyba: Argument typu 'number' není přiřaditelný parametru typu 'Lengthwise'.
Funkce loggingIdentity
přijímá argument typu T
, který musí mít vlastnost length
typu number. Tím je zajištěno, že funkce může bezpečně přistupovat k vlastnosti length
.
Generické třídy
Generika lze také použít s třídami:
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data = this.data.filter(d => d !== item);
}
getItems(): T[] {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Výstup: [ 'banana' ]
const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Výstup: [ 2 ]
Třída DataStorage
může ukládat data jakéhokoli typu T
. To vám umožňuje vytvářet znovupoužitelné datové struktury, které jsou typově bezpečné.
Generická rozhraní
Generická rozhraní jsou užitečná pro definování kontraktů, které mohou pracovat s různými typy. Například:
interface Result<T, E> {
success: boolean;
data?: T;
error?: E;
}
interface User {
id: number;
username: string;
email: string;
}
interface ErrorMessage {
code: number;
message: string;
}
function fetchUser(id: number): Result<User, ErrorMessage> {
if (id === 1) {
return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
} else {
return { success: false, error: { code: 404, message: "User not found" } };
}
}
const userResult = fetchUser(1);
if (userResult.success) {
console.log(userResult.data.username);
} else {
console.log(userResult.error.message);
}
Rozhraní Result
definuje generickou strukturu pro reprezentaci výsledku operace. Může obsahovat buď data typu T
, nebo chybu typu E
. Toto je běžný vzor pro zpracování asynchronních operací nebo operací, které mohou selhat.
Pomocné typy a generika
TypeScript poskytuje několik vestavěných pomocných typů, které dobře fungují s generiky. Tyto pomocné typy vám mohou pomoci transformovat a manipulovat s typy mocnými způsoby.
Partial<T>
Partial<T>
učiní všechny vlastnosti typu T
volitelnými:
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
const partialPerson: PartialPerson = { name: "Alice" }; // Platné
Readonly<T>
Readonly<T>
učiní všechny vlastnosti typu T
pouze pro čtení (readonly):
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Chyba: Nelze přiřadit k 'age', protože je to vlastnost pouze pro čtení.
Pick<T, K>
Pick<T, K>
vybere sadu vlastností K
z typu T
:
interface Person {
name: string;
age: number;
email: string;
}
type NameAndAge = Pick<Person, "name" | "age">;
const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };
Omit<T, K>
Omit<T, K>
odstraní sadu vlastností K
z typu T
:
interface Person {
name: string;
age: number;
email: string;
}
type PersonWithoutEmail = Omit<Person, "email">;
const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };
Record<K, T>
Record<K, T>
vytvoří typ s klíči K
a hodnotami typu T
:
type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Rozšířený seznam pro globální kontext
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Rozšířený seznam pro globální kontext
type CurrencyMap = Record<CountryCodes, Currency>;
const currencyMap: CurrencyMap = {
"US": "USD",
"CA": "CAD",
"UK": "GBP",
"DE": "EUR",
"FR": "EUR",
"JP": "JPY",
"CN": "CNY",
"IN": "INR",
"BR": "BRL",
"AU": "AUD",
};
Mapované typy
Mapované typy vám umožňují transformovat existující typy iterací přes jejich vlastnosti. Je to mocný způsob, jak vytvářet nové typy na základě existujících. Můžete například vytvořit typ, který učiní všechny vlastnosti jiného typu pouze pro čtení:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
};
const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Chyba: Nelze přiřadit k 'age', protože je to vlastnost pouze pro čtení.
V tomto příkladu [K in keyof Person]
iteruje přes všechny klíče rozhraní Person
a Person[K]
přistupuje k typu každé vlastnosti. Klíčové slovo readonly
činí každou vlastnost pouze pro čtení.
Podmíněné typy
Podmíněné typy vám umožňují definovat typy na základě podmínek. Je to mocný způsob, jak vytvářet typy, které se přizpůsobují různým scénářům.
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string
function getValue<T>(value: T): NonNullable<T> {
if (value == null) { // Zpracovává null i undefined
throw new Error("Hodnota nemůže být null nebo undefined");
}
return value as NonNullable<T>;
}
try {
const validValue = getValue("hello");
console.log(validValue.toUpperCase()); // Výstup: HELLO
const invalidValue = getValue(null); // Toto vyvolá chybu
console.log(invalidValue); // Tento řádek nebude dosažen
} catch (error: any) {
console.error(error.message); // Výstup: Hodnota nemůže být null nebo undefined
}
V tomto příkladu typ NonNullable<T>
kontroluje, zda je T
null
nebo undefined
. Pokud ano, vrací never
, což znamená, že typ není povolen. V opačném případě vrací T
. To vám umožňuje vytvářet typy, které jsou zaručeně ne-nullové.
Osvědčené postupy pro používání generik
Zde jsou některé osvědčené postupy, které je třeba mít na paměti při používání generik:
- Používejte popisné názvy typových parametrů: Vybírejte názvy, které jasně naznačují účel typového parametru.
- Používejte omezení k limitování typů, které lze použít s generickým typovým parametrem: Tím zajistíte, že váš generický kód může bezpečně pracovat se specifikovanými typy.
- Udržujte svůj generický kód jednoduchý a zaměřený: Vyhněte se přílišnému komplikování generického kódu mnoha typovými parametry nebo složitými omezeními.
- Důkladně dokumentujte svůj generický kód: Vysvětlete účel typových parametrů a jakákoli použitá omezení.
- Zvažte kompromisy mezi znovupoužitelností kódu a typovou bezpečností: I když generika mohou zlepšit znovupoužitelnost kódu, mohou také váš kód zkomplikovat. Zvažte výhody a nevýhody před použitím generik.
- Zvažte lokalizaci a globalizaci (l10n a g11n): Při práci s daty, která je třeba zobrazovat uživatelům v různých regionech, zajistěte, aby vaše generika podporovala vhodné formátování a kulturní konvence. Například formátování čísel a dat se může v různých lokalitách výrazně lišit.
Příklady v globálním kontextu
Pojďme se podívat na několik příkladů, jak lze generika použít v globálním kontextu:
Převod měny
interface ConversionRate {
rate: number;
fromCurrency: string;
toCurrency: string;
}
function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
return amount * rate.rate;
}
const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD se rovná ${amountInEUR} EUR`); // Výstup: 100 USD se rovná 85 EUR
Formátování data
interface DateFormatOptions {
locale: string;
options: Intl.DateTimeFormatOptions;
}
function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
return date.toLocaleDateString(format.locale, format.options);
}
const currentDate = new Date();
const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };
console.log("Datum v USA: " + formatDate(currentDate, usDateFormat));
console.log("Datum v Německu: " + formatDate(currentDate, germanDateFormat));
console.log("Datum v Japonsku: " + formatDate(currentDate, japaneseDateFormat));
Překladatelská služba
interface Translation {
[key: string]: string; // Umožňuje dynamické jazykové klíče
}
interface LanguageData<T extends Translation> {
languageCode: string;
translations: T;
}
const englishTranslations: Translation = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our website!"
};
const spanishTranslations: Translation = {
"hello": "Hola",
"goodbye": "Adiós",
"welcome": "¡Bienvenido a nuestro sitio web!"
};
const frenchTranslations: Translation = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre site web !"
};
const languageData: LanguageData<typeof englishTranslations>[] = [
{languageCode: "en", translations: englishTranslations },
{languageCode: "es", translations: spanishTranslations },
{languageCode: "fr", translations: frenchTranslations}
];
function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
const lang = languageData.find(lang => lang.languageCode === languageCode);
if (!lang) {
return `Překlad pro ${key} v ${languageCode} nebyl nalezen.`;
}
return lang.translations[key] || `Překlad pro ${key} nebyl nalezen.`;
}
console.log(translate("hello", "en", languageData)); // Výstup: Hello
console.log(translate("hello", "es", languageData)); // Výstup: Hola
console.log(translate("welcome", "fr", languageData)); // Výstup: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Výstup: Překlad pro missingKey v de nebyl nalezen.
Závěr
Generika v TypeScriptu jsou mocným nástrojem pro psaní znovupoužitelného, typově bezpečného kódu, který může pracovat s komplexními datovými typy. Porozuměním základní syntaxi, pokročilým funkcím a osvědčeným postupům pro používání generik můžete výrazně zlepšit kvalitu a udržovatelnost svých TypeScript aplikací. Při vývoji aplikací pro globální publikum vám generika mohou pomoci zpracovávat rozmanité datové formáty a kulturní konvence, čímž zajistíte bezproblémový uživatelský zážitek pro všechny.